﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace MetaMapics.Pbf
{
  public class PbfReader: IDisposable
  {
    private Stream stream;
    private PbfWireType wireType;

    public PbfReader(Stream stream)
    {
      this.stream = stream;
    }

    #region Standard Types
    public double ReadDouble()
    {
      if (wireType != PbfWireType.Bits64)
        throw new InvalidDataException();
      wireType = PbfWireType.Null;
      return PbfHelper.ReadDouble(stream);
    }

    public float ReadFloat()
    {
      if (wireType != PbfWireType.Bits32)
        throw new InvalidDataException();
      wireType = PbfWireType.Null;
      return PbfHelper.ReadFloat(stream);
    }

    public int ReadInt()
    {
      if (wireType != PbfWireType.VarInt)
        throw new InvalidDataException("Unexpected WireType");
      wireType = PbfWireType.Null;
      return (int)PbfHelper.ReadVarint(stream); //??
    }

    public uint ReadUint()
    {
      if (wireType != PbfWireType.VarInt)
        throw new InvalidDataException("Unexpected WireType");
      wireType = PbfWireType.Null;
      return PbfHelper.ReadVarint(stream); //??
    }

    public long ReadLong()
    {
      if (wireType != PbfWireType.VarInt)
        throw new InvalidDataException();
      wireType = PbfWireType.Null;
      return (long)PbfHelper.ReadLongVarint(stream);
    }

    public ulong ReadUlong()
    {
      if (wireType != PbfWireType.VarInt)
        throw new InvalidDataException();
      wireType = PbfWireType.Null;
      return PbfHelper.ReadLongVarint(stream);
    }

    public bool ReadBool()
    {
      if (wireType != PbfWireType.VarInt)
        throw new InvalidDataException();
      uint varint = PbfHelper.ReadVarint(stream);
      wireType = PbfWireType.Null;
      if (varint == 0)
        return false;
      if (varint == 1)
        return true;
      throw new InvalidDataException();
    }

    public string ReadString()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      var bytes = new byte[byteCount];
      for (int i = 0; i < byteCount; i++)
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      wireType = PbfWireType.Null;
      return Encoding.UTF8.GetString(bytes);
    }

    public byte[] ReadBytes()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      var bytes = new byte[byteCount];
      for (int i = 0; i < byteCount; i++)
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      wireType = PbfWireType.Null;
      return bytes;
    }
    #endregion

    #region Packed Standard Types
    public double[] ReadPackedDouble()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint dataCount = PbfHelper.ReadVarint(stream) / 8;
      var data = new double[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = PbfHelper.ReadDouble(stream);
      wireType = PbfWireType.Null;
      return data;
    }

    public float[] ReadPackedFloat()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint dataCount = PbfHelper.ReadVarint(stream) / 4;
      var data = new float[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = PbfHelper.ReadFloat(stream);
      wireType = PbfWireType.Null;
      return data;
    }

    public int[] ReadPackedInt()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      int varintByteCount;
      int dataCount = 0;
      int bytesUsed = 0;
      var dataBuffer = new int[byteCount];
      do
      {
        int varint = (int)PbfHelper.ReadVarint(stream, out varintByteCount);
        dataBuffer[dataCount++] = varint;
        bytesUsed += varintByteCount;
      }
      while (bytesUsed < byteCount);
      if (bytesUsed > byteCount)
        throw new InvalidDataException();
      var data = new int[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = dataBuffer[i];
      wireType = PbfWireType.Null;
      return data;
    }

    public long[] ReadPackedLong()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      int varintByteCount;
      int dataCount = 0;
      int bytesUsed = 0;
      var dataBuffer = new long[byteCount];
      do
      {
        long varint = (long)PbfHelper.ReadLongVarint(stream, out varintByteCount);
        dataBuffer[dataCount++] = varint;
        bytesUsed += varintByteCount;
      }
      while (bytesUsed < byteCount);
      if (bytesUsed > byteCount)
        throw new InvalidDataException();
      var data = new long[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = dataBuffer[i];
      wireType = PbfWireType.Null;
      return data;
    }

    public uint[] ReadPackedUint()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      int varintByteCount;
      int dataCount = 0;
      int bytesUsed = 0;
      var dataBuffer = new uint[byteCount];
      do
      {
        uint varint = PbfHelper.ReadVarint(stream, out varintByteCount);
        dataBuffer[dataCount++] = varint;
        bytesUsed += varintByteCount;
      }
      while (bytesUsed < byteCount);
      if (bytesUsed > byteCount)
        throw new InvalidDataException();
      var data = new uint[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = dataBuffer[i];
      wireType = PbfWireType.Null;
      return data;
    }

    public ulong[] ReadPackedUlong()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      int varintByteCount;
      int dataCount = 0;
      int bytesUsed = 0;
      var dataBuffer = new ulong[byteCount];
      do
      {
        ulong varint = PbfHelper.ReadLongVarint(stream, out varintByteCount);
        dataBuffer[dataCount++] = varint;
        bytesUsed += varintByteCount;
      }
      while (bytesUsed < byteCount);
      if (bytesUsed > byteCount)
        throw new InvalidDataException();
      var data = new ulong[dataCount];
      for (int i = 0; i < dataCount; i++)
        data[i] = dataBuffer[i];
      wireType = PbfWireType.Null;
      return data;
    }

    public bool[] ReadPackedBool()
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      var data = new bool[byteCount];
      for (int i = 0; i < data.Length; i++)
      {
        int b = stream.ReadByte();
        if (b < 0)
          throw new EndOfStreamException();
        if (b > 1)
          throw new InvalidDataException();
        data[i] = b == 1;
      }
      return data;
    }
    #endregion

    #region Alt Types
    public int ReadInt(PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Sfixed32)
      {
        if (wireType != PbfWireType.Bits32)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        var bytes = new byte[4];
        for (int i = 0; i < 4; i++)
        {
          int b = stream.ReadByte();
          if (b < 0)
            throw new EndOfStreamException();
          bytes[i] = (byte)b;
        }
        return BitConverter.ToInt32(bytes,0);
      }
      if (fieldType == PbfFieldType.Int32)
        return ReadInt();
      if (fieldType == PbfFieldType.Sint32)
      {
        if (wireType != PbfWireType.VarInt)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        int value = (int)PbfHelper.ReadVarint(stream);
        return PbfHelper.DecodeZigZag(value);
      }
      throw new ArgumentException();
    }

    public uint ReadUint(PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Fixed32)
      {
        if (wireType != PbfWireType.Bits32)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        var bytes = new byte[4];
        for (int i = 0; i < 4; i++)
        {
          int b = stream.ReadByte();
          if (b < 0)
            throw new EndOfStreamException();
          bytes[i] = (byte)b;
        }
        return BitConverter.ToUInt32(bytes, 0);
      }
      if (fieldType == PbfFieldType.Uint32)
        return ReadUint();
      throw new ArgumentException();
    }

    public long ReadLong(PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Sfixed64)
      {
        if (wireType != PbfWireType.Bits32)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        var bytes = new byte[8];
        for (int i = 0; i < 8; i++)
        {
          int b = stream.ReadByte();
          if (b < 0)
            throw new EndOfStreamException();
          bytes[i] = (byte)b;
        }
        return BitConverter.ToInt64(bytes, 0);
      }
      if (fieldType == PbfFieldType.Int64)
        return ReadLong();
      if (fieldType == PbfFieldType.Sint64)
      {
        if (wireType != PbfWireType.VarInt)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        long value = (long)PbfHelper.ReadLongVarint(stream);
        return PbfHelper.DecodeZigZag(value);
      }
      throw new ArgumentException();
    }

    public ulong ReadUlong(PbfFieldType fieldType)
    {
      if (fieldType == PbfFieldType.Fixed64)
      {
        if (wireType != PbfWireType.Bits32)
          throw new InvalidDataException();
        wireType = PbfWireType.Null;
        var bytes = new byte[8];
        for (int i = 0; i < 8; i++)
        {
          int b = stream.ReadByte();
          if (b < 0)
            throw new EndOfStreamException();
          bytes[i] = (byte)b;
        }
        return BitConverter.ToUInt64(bytes, 0);
      }
      if (fieldType == PbfFieldType.Uint64)
        return ReadUlong();
      throw new ArgumentException();
    }
    #endregion

    #region Packed Alt Types
    public int[] ReadPackedInt(PbfFieldType fieldType)
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);

      if (fieldType == PbfFieldType.Int32)
        return ReadPackedInt();

      if (fieldType == PbfFieldType.Sint32)
      {
        int varintByteCount;
        int dataCount = 0;
        int bytesUsed = 0;
        var dataBuffer = new int[byteCount];
        do
        {
          int varint = (int)PbfHelper.ReadVarint(stream, out varintByteCount);
          dataBuffer[dataCount++] = PbfHelper.DecodeZigZag(varint);
          bytesUsed += varintByteCount;
        }
        while (bytesUsed < byteCount);
        if (bytesUsed > byteCount)
          throw new InvalidDataException();
        var data = new int[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = dataBuffer[i];
        wireType = PbfWireType.Null;
        return data;
      }

      if (fieldType == PbfFieldType.Sfixed32)
      {
        uint dataCount = byteCount / 4;
        var data = new int[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = PbfHelper.ReadBits32(stream);
        return data;
      }
      throw new ArgumentException();
    }

    public uint[] ReadPackedUint(PbfFieldType fieldType)
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);

      if (fieldType == PbfFieldType.Int32)
        return ReadPackedUint();
     
      if (fieldType == PbfFieldType.Fixed32)
      {
        uint dataCount = byteCount / 4;
        var data = new uint[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = (uint)PbfHelper.ReadBits32(stream);
        return data;
      }
      throw new ArgumentException();
    }

    public long[] ReadPackedLong(PbfFieldType fieldType)
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);

      if (fieldType == PbfFieldType.Int64)
        return ReadPackedLong();

      if (fieldType == PbfFieldType.Sint64)
      {
        int varintByteCount;
        int dataCount = 0;
        int bytesUsed = 0;
        var dataBuffer = new long[byteCount];
        do
        {
          long varint = (long)PbfHelper.ReadLongVarint(stream, out varintByteCount);
          dataBuffer[dataCount++] = PbfHelper.DecodeZigZag(varint);
          bytesUsed += varintByteCount;
        }
        while (bytesUsed < byteCount);
        if (bytesUsed > byteCount)
          throw new InvalidDataException();
        var data = new long[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = dataBuffer[i];
        wireType = PbfWireType.Null;
        return data;
      }

      if (fieldType == PbfFieldType.Sfixed64)
      {
        uint dataCount = byteCount / 8;
        var data = new long[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = PbfHelper.ReadBits64(stream);
        return data;
      }
      throw new ArgumentException();
    }

    public ulong[] ReadPackedUlong(PbfFieldType fieldType)
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);

      if (fieldType == PbfFieldType.Int64)
        return ReadPackedUlong();

      if (fieldType == PbfFieldType.Fixed64)
      {
        uint dataCount = byteCount / 8;
        var data = new ulong[dataCount];
        for (int i = 0; i < dataCount; i++)
          data[i] = (ulong)PbfHelper.ReadBits64(stream);
        return data;
      }
      throw new ArgumentException();
    }
    #endregion

    #region Other Methods
    //public ulong ReadNestedMessageLength()
    //{
    //  if (wireType != PbfWireType.LengthDelimited)
    //    throw new InvalidDataException();
    //  return ReadLongVarint(stream);
    //}

    public void ReadNestedMessage(IPbfStreamable dataObject)
    {
      if (wireType != PbfWireType.LengthDelimited)
        throw new InvalidDataException();
      uint byteCount = PbfHelper.ReadVarint(stream);
      var buffer = new byte[byteCount];
      int bytesRead = stream.Read(buffer, 0, (int)byteCount);
      using (var memStream = new MemoryStream(buffer))
      using (var pbfReader = new PbfReader(memStream))
        dataObject.ReadFromPbf(pbfReader);
      wireType = PbfWireType.Null;
    }

    public void IgnoreField()
    {
      if (wireType == PbfWireType.VarInt)
        PbfHelper.ReadLongVarint(stream);
      else if (wireType == PbfWireType.Bits32)
        PbfHelper.ReadFloat(stream);
      else if (wireType == PbfWireType.Bits64)
        PbfHelper.ReadDouble(stream);
      else if (wireType == PbfWireType.LengthDelimited)
        ReadBytes();
      wireType = PbfWireType.Null;
    }

    public IEnumerable<uint> Fields()
    {
      bool success;
      do
      {
        uint tag;
        success = PbfHelper.TryReadVarint(stream, out tag);
        if (success)
        {
          uint fieldNum = tag >> 3;
          wireType = (PbfWireType)(tag & 0x7);
          yield return fieldNum;
        }
      }
      while (success);
    }

    public uint ReadTag()
    {
      uint tag = PbfHelper.ReadVarint(stream);
      wireType = (PbfWireType)(tag & 0x7); 
      return tag >> 3;
    }

    public void Dispose()
    {
      if (stream != null)
        stream.Dispose();
      stream = null;
    }

    public void Close()
    {
      Dispose();
    }
    #endregion
  }
}
